AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみる
はじめに
先日のS3フォルダの削除に引き続き、S3フォルダをコピーしてみます。先日の削除と同様にコピー対象のオブジェクトのリストが取得した後、個々のオブジェクトをコピーするプロブラムを書く必要がありました。これも毎回関数を作成するのは面倒なので、いざというときに備えて作成しましたのでご紹介します。
検証用データ
以下のような階層のデータを事前に作成しました。
$ aws s3 ls s3://s3-copy-folder/ --recursive 2018-06-29 20:24:28 0 source-folder/ 2018-06-29 21:08:40 0 source-folder/source-sub-folder/ 2018-06-29 21:09:28 0 source-folder/source-sub-folder/test-sub.txt 2018-06-29 20:27:08 0 source-folder/test.txt
AWSCLIでS3フォルダのコピーする
サブコマンドcpによるS3フォルダのコピー
S3フォルダ名を指定するとエラーになり、コピーができません。
$ aws s3 cp s3://s3-copy-folder/source-folder s3://s3-delete-folder/target-folder fatal error: An error occurred (404) when calling the HeadObject operation: Key "source-folder" does not exist
S3フォルダ名を指定してコピーするには、--recursive
を指定します。S3フォルダごとコピーされたことが確認できます。
$ aws s3 cp s3://s3-copy-folder/source-folder s3://s3-delete-folder/target-folder --recursive copy: s3://s3-copy-folder/source-folder/test.txt to s3://s3-delete-folder/target-folder/test.txt copy: s3://s3-copy-folder/source-folder/source-sub-folder/test-sub.txt to s3://s3-delete-folder/target-folder/source-sub-folder/test-sub.txt
2999ファイルのコピー時間を計測します。
$ time aws s3 cp s3://s3-delete-folder/my_folder/folder_1 s3://s3-delete-folder/target-folder --recursive copy: s3://s3-delete-folder/my_folder/folder_1/test_1001 to s3://s3-delete-folder/target-folder/test_1001 copy: s3://s3-delete-folder/my_folder/folder_1/aaaa_1.txt to s3://s3-delete-folder/target-folder/aaaa_1.txt copy: s3://s3-delete-folder/my_folder/folder_1/test_1 to s3://s3-delete-folder/target-folder/test_1 : copy: s3://s3-delete-folder/my_folder/folder_1/test_989 to s3://s3-delete-folder/target-folder/test_989 real 0m28.670s user 0m12.608s sys 0m1.201s
サブコマンドsyncによるS3フォルダのコピー
syncは、2つのフォルダ間のオブジェクトを同期します。同じく、2999ファイルのコピー時間を計測しましたが、速度はサブコマンドcpと殆ど差がありません。
$ time aws s3 sync s3://s3-delete-folder/my_folder/folder_1 s3://s3-delete-folder/target-folder copy: s3://s3-delete-folder/my_folder/folder_1/aaaa_1.txt to s3://s3-delete-folder/target-folder/aaaa_1.txt copy: s3://s3-delete-folder/my_folder/folder_1/test_1002 to s3://s3-delete-folder/target-folder/test_1002 copy: s3://s3-delete-folder/my_folder/folder_1/test_1006 to s3://s3-delete-folder/target-folder/test_1006 : copy: s3://s3-delete-folder/my_folder/folder_1/test_949 to s3://s3-delete-folder/target-folder/test_949 real 0m29.505s user 0m12.450s sys 0m1.189s
もう一度、サブコマンドsyncを実行するとすでにコピー済みなのでコピーしません。サブコマンドcpは実行するたびに同じファイルをコピーしますが、サブコマンドsyncはすでにコピー済みであるかのチェックがおこり、無駄なコピーが不要になり、結果として処理が速く終わります。
$ time aws s3 sync s3://s3-delete-folder/my_folder/folder_1 s3://s3-delete-folder/target-folder real 0m5.245s user 0m1.714s sys 0m0.132s
また、サブコマンドsyncの場合、万が一コピーが中断したとしても、もう一度実行することで続きのコピーができる点で優れています。
補足:オブジェクト数のカウント
なお、オブジェクト数は以下のコマンドで高速に取得できます。下記の方法では、S3基盤でオブジェクト数をカウントし、レスポンスにはオブジェクト数のみが含まれるため、オブジェクトリスト全体を取得するよりは高速に動作します。
$ time aws s3api list-objects-v2 --bucket s3-delete-folder --prefix my_folder/folder_1 --query 'length(Contents)' 2999 real 0m3.156s user 0m0.640s sys 0m0.144s
Python(boto3)でS3フォルダ間でコピーする方法
S3フォルダをまとめてコピーするには
S3フォルダをまとめてコピーするには、まずファイルの一覧を取得した後、オブジェクトごとにコピーを実行する必要があります。しかし、バケットとキー指定してオブジェクトの一覧を取得する関数(list_objects や list_objects_v2)では、一度に最大1000個までしか取得できません。そのため1000以上のオブジェクトを考慮して、リストの終わりまで繰り返し処理するような実装が必要です。
オブジェクトの一覧を取得する関数は2種類あり、従来のlist_objects関数 と 現在のlist_objects_v2関数の2種類ありますので、それぞれのAPIで一覧を取得し、オブジェクトを削除する関数を作成しました。新規でスクリプトを作成する場合は、現在のlist_objects_v2関数で構いませんが、比較的古いBoto3の場合は、従来のlist_objects関数を用いた関数を利用したほうが良いでしょう。
S3フォルダのコピーでは、指定したフォルダの下のオブジェクトのすべてをコピーしますので、ソースフォルダの相対パスを取得して、コピーしなけれなりません。そうすることで、AWSCLIのcp
コマンドの--recursive
指定したときと同じ動作を再現しています。
[推奨]list_objects_v2関数によるS3フォルダ間のコピー
現在のAPI(list_objects_v2)では取得されたオブジェクト件数が、リクエストパラメータで指定されたMaxKeysか、デフォルト値(1000)以上存在した場合には、 NextContinuationTokenという値がレスポンスで返却されていました。その値を次のAPIコールにContinuationTokenパラメータとして与えることで残りの値を取得することができます。v1のサポートも続けられていますが、今後の新規開発ではv2を利用することが推奨されています。
ソースデータにソースバケット名(s3-copy-folder)とソースプレフィックス(source_folder/)、ターゲットデータにターゲットバケット名(s3-target-folder)とターゲットプレフィックス(target_folder/)、を指定して実行します。プレフィックスは共にフォルダを指定するので、プレフィックスの最後の文字はスラッシュ「/」にしてください。
#!/usr/bin/env python # -*- coding: utf-8 -*- import boto3 import re source_bucket = 's3-copy-folder' source_prefix = 'source-folder/' target_bucket = 's3-target-folder' target_prefix = 'target-folder/' def copy_all_keys_v2(source_bucket='', source_prefix='', target_bucket='', target_prefix='', dryrun=False): contents_count = 0 next_token = '' while True: if next_token == '': response = s3client.list_objects_v2(Bucket=source_bucket, Prefix=source_prefix) else: response = s3client.list_objects_v2(Bucket=source_bucket, Prefix=source_prefix, ContinuationToken=next_token) if 'Contents' in response: contents = response['Contents'] contents_count = contents_count + len(contents) for content in contents: relative_prefix = re.sub('^' + source_prefix, '', content['Key']) if not dryrun: print('Copying: s3://' + source_bucket + '/' + content['Key'] + ' To s3://' + target_bucket + '/' + target_prefix + relative_prefix) s3client.copy_object(Bucket=target_bucket, Key=target_prefix + relative_prefix, CopySource={'Bucket': source_bucket, 'Key': content['Key']}) else: print('DryRun: s3://' + source_bucket + '/' + content['Key'] + ' To s3://' + target_bucket + '/' + target_prefix + relative_prefix) if 'NextContinuationToken' in response: next_token = response['NextContinuationToken'] else: break print(contents_count) if __name__ == "__main__": s3client = boto3.client('s3') copy_all_keys_v2(source_bucket, source_prefix, target_bucket, target_prefix, True)
list_objects_v2関数の詳細については、以下のブログを御覧ください。
[非推奨]旧API list_objects関数によるS3フォルダ間のコピー
従来のAPI(list_objects)では取得されたオブジェクト件数が、リクエストパラメータで指定されたmax-itemsか、デフォルト値(1000)以上存在した場合には、NextMarkerという値がレスポンスで返却されていました。その値を次のAPIコールにmarkerパラメータとして与えることで残りの値を取得することができました。
ソースデータにソースバケット名(s3-copy-folder)とソースプレフィックス(source_folder/)、ターゲットデータにターゲットバケット名(s3-target-folder)とターゲットプレフィックス(target_folder/)、を指定して実行します。プレフィックスは共にフォルダを指定するので、プレフィックスの最後の文字はスラッシュ「/」にしてください。
#!/usr/bin/env python # -*- coding: utf-8 -*- import boto3 import re source_bucket = 's3-copy-folder' source_prefix = 'source-folder/' target_bucket = 's3-target-folder' target_prefix = 'target-folder/' def copy_all_keys(source_bucket='', source_prefix='', target_bucket='', target_prefix='', dryrun=False): contents_count = 0 marker = None while True: if marker: response = s3client.list_objects(Bucket=source_bucket, Prefix=source_prefix, Marker=marker) else: response = s3client.list_objects(Bucket=source_bucket, Prefix=source_prefix) if 'Contents' in response: contents = response['Contents'] contents_count = contents_count + len(contents) for content in contents: relative_prefix = re.sub('^' + source_prefix, '', content['Key']) if not dryrun: print('Copying: s3://' + source_bucket + '/' + content['Key'] + ' To s3://' + target_bucket + '/' + target_prefix + relative_prefix) s3client.copy_object(Bucket=target_bucket, Key=target_prefix + relative_prefix, CopySource={'Bucket': source_bucket, 'Key': content['Key']}) else: print('DryRun: s3://' + source_bucket + '/' + content['Key'] + ' To s3://' + target_bucket + '/' + target_prefix + relative_prefix) if response['IsTruncated']: marker = response['Contents'][-1]['Key'] else: break print(contents_count) if __name__ == '__main__': s3client = boto3.client('s3') copy_all_keys(source_bucket, source_prefix, target_bucket, target_prefix, True)
最後に
S3フォルダ間のコピーは、マネジメントコンソールから操作できませんが、AWSCLIでは--recursive
指定することでコピーできます。できるだけこれと同じことができるようにしたのがboto3を用いたS3フォルダ間のコピーです。
私は、AWS Glueでターゲットフォルダに直接更新を加えるのではなく、ステージングフォルダに結果を作成します。変換が成功すれば、既存のフォルダを削除した後、ステージングフォルダの内容をコピーします。そうすることで変換に失敗したときのロールバックを可能にします。S3フォルダ間のコピーが済んだら、先日のS3フォルダの削除で直ちに削除します。なお、実際のプログラムに組み込む際には、DryRunを実行して削除対象の一覧を確認して、削除対象の確認を怠らないでくださいね。
なお、何度も「S3フォルダ」なんて言ってましたが、フォルダなんてないものはないことは承知しています。